﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using Xenta.Attributes;
using Xenta.Entities;
using Xenta.Enums;
using Xenta.Utils;

namespace Xenta.Plugins
{
    [Dependency("cf431931-cf9a-454c-bf7a-738f34b8ca44", 
        Name = "Manual")]
    public sealed class ManualPaymentProcessor : IPaymentProcessor
    {
        public bool CanApplyTo(InvoiceEntity invoice)
        {
            return invoice.Order.Seller.Account
                .Attribute(Par.In("Plugin").Of(this)
                .Name("Enabled")).ParseAs(false);
        }

        public decimal CalcHandlingFee(InvoiceEntity invoice, decimal paymentAmount)
        {
            return Decimal.Zero;
        }

        public IDictionary<String, String> Void(PaymentEntity payment, out string comment)
        {
            comment = String.Empty;
            return null;
        }

        public IDictionary<String, String> Place(PaymentEntity payment, out string comment)
        {
            comment = String.Empty;
            return null;
        }

        public IDictionary<String, String> Refund(PaymentEntity payment, out string comment)
        {
            comment = String.Empty;
            return null;
        }

        public IDictionary<String, String> Authorize(PaymentEntity payment, out string comment)
        {
            comment = String.Empty;
            return null;
        }

        public IDictionary<String, String> Capture(PaymentEntity payment, out string comment)
        {
            comment = String.Empty;
            return null;
        }
    }

    [Dependency("2cd39234-9ba5-4b0a-9104-46c3ed3b07d1",
        Name = "2Checkout")]
    public sealed class TwoCheckoutPaymentProcessor : IPaymentProcessor, IIPNHandler
    {
        #region Constants

        public const string GATEWAY_URL = "https://www.2checkout.com/checkout/purchase";

        #endregion

        #region Processor

        public bool CanApplyTo(InvoiceEntity invoice)
        {
            return invoice.Order.Seller.Account
                .Attribute(Par.In("Plugin").Of(this)
                .Name("Enabled")).ParseAs(false);
        }

        public decimal CalcHandlingFee(InvoiceEntity invoice, decimal paymentAmount)
        {
            var seller = invoice.Order.Seller;
            decimal txFee = seller.Account.Attribute(Par.In("Plugin").Of(this)
                .Name("TransactionFee"))
                .ParseAs(0.45m);
            decimal rate = seller.Account.Attribute(Par.In("Plugin").Of(this)
                .Name("Rate"))
                .ParseAs(5.5m);
            return (paymentAmount / 100 * rate) + txFee;
        }

        public IDictionary<String, String> Void(PaymentEntity payment, out string comment)
        {
            comment = String.Empty;
            return null;
        }

        public IDictionary<String, String> Place(PaymentEntity payment, out string comment)
        {
            comment = String.Empty;

            var invoice = payment.Invoice;
            var order = invoice.Order;
            var seller = order.Seller;
            string sid = seller.Account.Attribute(Par.In("Plugin").Of(this).Name("SID"));
            if(String.IsNullOrWhiteSpace(sid))
                throw Err.Of(this).NotConfigured().Ex();

            var customer = payment.Customer;
            var billing = order.BillingAddress;
            var ret = new Dictionary<String, String>
            {
                {"gateway", GATEWAY_URL},
                {"sid", sid},
                {"fixed", "Y"},
                {"id_type", "1"},
                {"lang", customer.Account.LocalizationProfile.Language.Code},
                {"cart_order_id", payment.Guid.ToString()},
                {"merchant_order_id", payment.Guid.ToString()},
                {"total", payment.Amount.ToString("F2", CultureInfo.InvariantCulture)},
                {"card_holder_name", billing.ContactName},
                {"street_address", billing.Address1},
                {"street_address2", billing.Address2},
                {"city", billing.City},
                {"state", billing.Region.Name},
                {"zip", billing.PostalCode},
                {"country", billing.Country.Name},
                {"email", billing.Email},
                {"phone", billing.PhoneNumber},
            };
            if(seller.Account.Attribute(Par.In("Plugin").Of(this).Name("Demo")).ParseAs(true))
                ret["demo"] = "Y";
            return ret;
        }

        public IDictionary<String, String> Refund(PaymentEntity payment, out string comment)
        {
            comment = String.Empty;
            return null;
        }

        public IDictionary<String, String> Authorize(PaymentEntity payment, out string comment)
        {
            comment = String.Empty;
            return null;
        }

        public IDictionary<String, String> Capture(PaymentEntity payment, out string comment)
        {
            comment = String.Empty;
            return null;
        }

        #endregion

        #region IPN
        
        public string Identify(IDictionary<String, String> ipn,
            out Guid guid)
        {
            guid = Guid.Empty;
            if(ipn == null || !ipn.Any())
                return Err.Of(this).No("IPN");
            if(ipn.ContainsKey("vendor_order_id"))
                guid = ipn["vendor_order_id"].ParseAs(Guid.Empty);
            else if(ipn.ContainsKey("merchant_order_id"))
                guid = ipn["merchant_order_id"].ParseAs(Guid.Empty);
            else
                return Err.Of(this).NotSpecified("vendor_order_id|merchant_order_id");
            return String.Empty;
        }

        public string Handle(IDictionary<String, String> ipn,
            PaymentEntity payment,
            out PaymentFlags status)
        {
            status = PaymentFlags.None;
            if(ipn == null || !ipn.Any())
                return Err.Of(this).No("IPN");
            
            //check SID
            string sidRcv = String.Empty;
            if(ipn.ContainsKey("vendor_id"))
                sidRcv = ipn["vendor_id"];
            else if(ipn.ContainsKey("sid"))
                sidRcv = ipn["sid"];
            else
                return Err.Of(this).NotSpecified("vendor_id|sid");
            var seller = payment.Invoice.Order.Seller;
            string sid = seller.Account.Attribute(Par.In("Plugin").Of(this).Name("SID"));
            if(sid != sidRcv)
                return Err.Of(this).NotValid("sid");

            //check MD5 hash
            string md5HashRcv = String.Empty;
            if(ipn.ContainsKey("md5_hash"))
                md5HashRcv = ipn["md5_hash"];
            else if(ipn.ContainsKey("key"))
                md5HashRcv = ipn["key"];
            else
                return Err.Of(this).NotSpecified("md5_hash|key");
            md5HashRcv = md5HashRcv.ToUpperInvariant();

            string secretWord = seller.Account.Attribute(Par.In("Plugin").Of(this).Name("SecretWord"));
            string strToHash = String.Empty;
            if(ipn.ContainsKey("sale_id"))
            {
                if(!ipn.ContainsKey("invoice_id"))
                    return Err.Of(this).NotSpecified("invoice_id");
                strToHash = String.Format("{0}{1}{2}{3}",
                    ipn["sale_id"],
                    sid,
                    ipn["invoice_id"],
                    secretWord);
            }
            else if(ipn.ContainsKey("order_number"))
            {
                strToHash = String.Format(CultureInfo.InvariantCulture,
                    "{0}{1}{2}{3:F2}",
                    secretWord,
                    sid,
                    !seller.Account.Attribute(Par.In("Plugin").Of(this)
                        .Name("Demo")).ParseAs(true) ?
                            ipn["order_number"]
                            : "1",
                    payment.Amount);
            }
            else
                return Err.Of(this).NotSpecified("sale_id|order_number");
            string md5Hash = CryptoUtility.MD5Hash(strToHash, Encoding.ASCII).ToHexString();
            if(md5Hash != md5HashRcv)
                return Err.Of(this).NotAuthorized("IPN");

            //get status
            string msgType = ipn.ContainsKey("message_type") 
                ? ipn["message_type"]
                : "ORDER_CREATED";
            msgType = msgType.ToUpperInvariant();
            if(msgType == "ORDER_CREATED" || msgType == "INVOICE_STATUS_CHANGED")
            {
                string invStatus = ipn.ContainsKey("invoice_status") 
                    ? ipn["invoice_status"]
                    : "APPROVED";
                invStatus = invStatus.ToUpperInvariant();
                switch(invStatus)
                {
                    case "APPROVED":
                    case "PENDING":
                        status = PaymentFlags.Authorized;
                        break;
                    case "DEPOSITED":
                        status = PaymentFlags.Captured;
                        break;
                }
            }
            else if(msgType.Equals("REFUND_ISSUED"))
                status = PaymentFlags.Refunded;

            return String.Empty;
        }

        #endregion
    }

    [Dependency("15498e59-55f6-4039-b42f-a262235c601c",
        Name = "Authorize.Net")]
    public sealed class AuthorizeNetPaymentProcessor : IPaymentProcessor, IIPNHandler
    {
        #region Constants

        public const string GATEWAY_PROD_URL = "https://secure.authorize.net/gateway/transact.dll";
        public const string GATEWAY_TEST_URL = "https://test.authorize.net/gateway/transact.dll";

        #endregion

        #region Processor

        public bool CanApplyTo(InvoiceEntity invoice)
        {
            return invoice.Order.Seller.Account
                .Attribute(Par.In("Plugin").Of(this)
                .Name("Enabled")).ParseAs(false);
        }

        public decimal CalcHandlingFee(InvoiceEntity invoice, decimal paymentAmount)
        {
            return invoice.Order.Seller.Account.Attribute(Par.In("Plugin").Of(this)
                .Name("TransactionFee"))
                .ParseAs(0.10m);
        }

        public IDictionary<String, String> Void(PaymentEntity payment, out string comment)
        {
            comment = String.Empty;
            return null;
        }

        public IDictionary<String, String> Place(PaymentEntity payment, out string comment)
        {
            comment = String.Empty;
            var invoice = payment.Invoice;
            var order = invoice.Order;
            var customer = payment.Customer;
            var seller = order.Seller;
            //check if configured
            string login = seller.Account.Attribute(Par.In("Plugin").Of(this).Name("APILoginID"));
            string txKey = seller.Account.Attribute(Par.In("Plugin").Of(this).Name("TransactionKey"));
            if(String.IsNullOrWhiteSpace(login) || String.IsNullOrWhiteSpace(txKey))
                throw Err.Of(this).NotConfigured().Ex();
            bool testMode = seller.Account.Attribute(Par.In("Plugin").Of(this).Name("TestMode")).ParseAs(true);
            string txType = seller.Account.Attribute(Par.In("Plugin").Of(this).Name("TransactionType"));
            if(String.IsNullOrWhiteSpace(txType))
                txType = "AUTH_CAPTURE";
            long timestamp = DateHelper.ToUnixTs(DateTime.UtcNow);
            //calc hash
            string strToHash = String.Format(CultureInfo.InvariantCulture,
                "{0}^{1}^{2}^{3:F2}^",
                login,
                payment.EntityID,
                timestamp,
                payment.Amount);
            string md5Hash = CryptoUtility.HMACMD5Hash(txKey, strToHash, Encoding.ASCII)
                .ToHexString();
            var billing = order.BillingAddress;
            return new Dictionary<String, String>
            {
                {"gateway", testMode ? GATEWAY_TEST_URL : GATEWAY_PROD_URL},
                {"x_fp_hash", md5Hash},
                {"x_fp_sequence", payment.EntityID.ToString()},
                {"x_fp_timestamp", timestamp.ToString()},
                {"x_login", login},
                {"x_type", txType},
                {"x_show_form", "PAYMENT_FORM"},
                {"x_relay_response", "TRUE"},
                {"x_delim_data", "FALSE"},
                {"x_amount", payment.Amount.ToString("F2", CultureInfo.InvariantCulture)},
                {"x_invoice_num", invoice.EntityID.ToString()},
                {"x_description", payment.Guid.ToString()},
                {"x_cust_id", customer.EntityID.ToString()},
                {"x_address", billing.Address1},
                {"x_city", billing.City},
                {"x_state", billing.Region.Name},
                {"x_zip", billing.PostalCode},
                {"x_country", billing.Country.Name},
                {"x_phone", billing.PhoneNumber},
                {"x_email", billing.Email},
                {"x_test_request", testMode ? "TRUE" : "FALSE"}
            };
        }

        public IDictionary<String, String> Refund(PaymentEntity payment, out string comment)
        {
            comment = String.Empty;
            return null;
        }

        public IDictionary<String, String> Authorize(PaymentEntity payment, out string comment)
        {
            comment = String.Empty;
            return null;
        }

        public IDictionary<String, String> Capture(PaymentEntity payment, out string comment)
        {
            comment = String.Empty;
            return null;
        }

        #endregion

        #region IPN
        
        public string Identify(IDictionary<String, String> ipn,
            out Guid guid)
        {
            guid = Guid.Empty;
            if(ipn == null || !ipn.Any())
                return Err.Of(this).No("IPN");
            if(!ipn.ContainsKey("x_description"))
                return Err.Of(this).NotSpecified("x_description");
            guid = ipn["x_description"].ParseAs(Guid.Empty);
            return String.Empty;
        }

        public string Handle(IDictionary<String, String> ipn,
            PaymentEntity payment,
            out PaymentFlags status)
        {
            status = PaymentFlags.None;

            if(ipn == null || !ipn.Any())
                return Err.Of(this).No("IPN");
            if(!ipn.ContainsKey("x_response_code"))
                return Err.Of(this).NotSpecified("x_response_code");
            if(!ipn.ContainsKey("x_type"))
                return Err.Of(this).NotSpecified("x_type");
            if(!ipn.ContainsKey("x_invoice_num"))
                return Err.Of(this).NotSpecified("x_invoice_num");
            if(!ipn.ContainsKey("x_MD5_Hash"))
                return Err.Of(this).NotSpecified("x_MD5_Hash");
            if(!ipn.ContainsKey("x_trans_id"))
                return Err.Of(this).NotSpecified("x_trans_id");
            //check hash
            var seller = payment.Invoice.Order.Seller;
            string login = seller.Account.Attribute(Par.In("Plugin").Of(this).Name("APILoginID"));
            string md5HashValue = seller.Account.Attribute(Par.In("Plugin").Of(this).Name("MD5HashValue"));
            string txId = ipn["x_trans_id"];
            string strToHash = String.Format(CultureInfo.InvariantCulture,
                "{0}{1}{2}{3:F2}",
                md5HashValue,
                login,
                txId,
                payment.Amount);
            string md5Hash = CryptoUtility.MD5Hash(strToHash, Encoding.ASCII).ToHexString();
            string md5HashRecv = ipn["x_MD5_Hash"].ToUpperInvariant();
            if(md5Hash != md5HashRecv)
                return Err.Of(this).NotAuthorized("IPN");
            //get status
            int rspCode = ipn["x_response_code"].ParseAs(3);
            if(rspCode == 3)
            {
                return Err.Of(this).Code(ipn.ContainsKey("x_response_reason_code")
                    ? ipn["x_response_reason_code"]
                    : "Uknown");
            }
            if(rspCode == 1)
            {
                string txType = ipn["x_type"];
                if(String.IsNullOrWhiteSpace(txType))
                    return Err.Of(this).NotValid("x_type");
                txType = txType.ToUpperInvariant();
                if(txType == "AUTH_ONLY")
                    status = PaymentFlags.Authorized;
                else if(txType == "AUTH_CAPTURE")
                    status = PaymentFlags.Captured;
                else if(txType == "VOID")
                    status = PaymentFlags.Voided;
            }
            return String.Empty;
        }

        #endregion
    }

    [Dependency("87f016ca-2c38-4f1d-b10e-9c896ba58ed5",
        Name = "PayPal Standard")]
    public sealed class PayPalStandardPaymentProcessor : IPaymentProcessor, IIPNHandler
    {
        #region Constants

        public const string GATEWAY_PROD_URL = "https://www.paypal.com/cgi-bin/webscr";
        public const string GATEWAY_TEST_URL = "https://www.sandbox.paypal.com/us/cgi-bin/webscr";

        #endregion

        #region Processor

        public bool CanApplyTo(InvoiceEntity invoice)
        {
            return invoice.Order.Seller.Account
                .Attribute(Par.In("Plugin").Of(this)
                .Name("Enabled")).ParseAs(false);
        }

        public decimal CalcHandlingFee(InvoiceEntity invoice, decimal paymentAmount)
        {
            var seller = invoice.Order.Seller;
            decimal txFee = seller.Account.Attribute(Par.In("Plugin").Of(this)
                .Name("TransactionFee"))
                .ParseAs(0.3m);
            decimal rate = seller.Account.Attribute(Par.In("Plugin").Of(this)
                .Name("Rate"))
                .ParseAs(2.9m);
            return (paymentAmount / 100 * rate) + txFee;
        }

        public IDictionary<String, String> Void(PaymentEntity payment, out string comment)
        {
            comment = String.Empty;
            return null;
        }

        public IDictionary<String, String> Place(PaymentEntity payment, out string comment)
        {
            comment = String.Empty;
            var invoice = payment.Invoice;
            var order = invoice.Order;
            var customer = payment.Customer;
            var seller = order.Seller;
            var billing = order.BillingAddress;
            string business = seller.Account.Attribute(Par.In("Plugin").Of(this).Name("Business"));
            if(String.IsNullOrWhiteSpace(business))
                throw Err.Of(this).NotConfigured().Ex();
            bool testMode = seller.Account.Attribute(Par.In("Plugin").Of(this).Name("TestMode")).ParseAs(true);
            string action = seller.Account.Attribute(Par.In("Plugin").Of(this).Name("PaymentAction"));
            if(String.IsNullOrWhiteSpace(action))
                action = "sale";
            return new Dictionary<String, String>
            {
                {"gateway", testMode ? GATEWAY_TEST_URL : GATEWAY_PROD_URL},
                {"cmd", "_xclick"},
                {"business", business},
                {"paymentaction", action},
                {"upload", "1"},
                {"item_name", payment.Guid.ToString()},
                {"amount", payment.Amount.ToString("F2", CultureInfo.InvariantCulture)},
                {"invoice", payment.Invoice.EntityID.ToString()},
                {"custom", payment.Guid.ToString()},
                {"lc", customer.Account.LocalizationProfile.Language.Code},
                {"address1", billing.Address1},
                {"address2", billing.Address2},
                {"city", billing.City},
                {"country", billing.Country.Code},
                {"email", billing.Email},
                {"zip", billing.PostalCode}
            };
        }

        public IDictionary<String, String> Refund(PaymentEntity payment, out string comment)
        {
            comment = String.Empty;
            return null;
        }

        public IDictionary<String, String> Authorize(PaymentEntity payment, out string comment)
        {
            comment = String.Empty;
            return null;
        }

        public IDictionary<String, String> Capture(PaymentEntity payment, out string comment)
        {
            comment = String.Empty;
            return null;
        }

        #endregion

        #region IPN

        public string Identify(IDictionary<String, String> ipn,
            out Guid guid)
        {
            guid = Guid.Empty;

            if(ipn == null || !ipn.Any())
                return Err.Of(this).No("IPN");
            if(!ipn.ContainsKey("custom"))
                return Err.Of(this).NotSpecified("custom");

            guid = ipn["custom"].ParseAs(Guid.Empty);

            return String.Empty;
        }

        public string Handle(IDictionary<String, String> ipn,
            PaymentEntity payment,
            out PaymentFlags status)
        {
            status = PaymentFlags.None;

            if(ipn == null || !ipn.Any())
                return Err.Of(this).No("IPN");
            if(!ipn.ContainsKey("payment_status"))
                return Err.Of(this).NotSpecified("payment_status");

            //verify
            bool testMode = payment.Invoice.Order.Seller.Account.Attribute(Par.In("Plugin").Of(this).Name("TestMode")).ParseAs(true);
            var sb = new StringBuilder("cmd=_notify-validate");
            foreach(var kv in ipn)
                sb.AppendFormat("&{0}={1}", kv.Key, kv.Value);
            string ret = String.Empty;
            try
            {
                var req = WebRequest.Create(testMode ? GATEWAY_TEST_URL : GATEWAY_PROD_URL)
                    as HttpWebRequest;
                req.Method = "POST";
                req.ContentType = "application/x-www-form-urlencoded";
                req.ContentLength = sb.Length;
                using(var writer = new StreamWriter(req.GetRequestStream(), Encoding.ASCII))
                    writer.Write(sb.ToString());
                using(var rsp = req.GetResponse())
                {
                    using(var reader = new StreamReader(rsp.GetResponseStream()))
                        ret = reader.ReadToEnd();
                }
            }
            catch(Exception ex)
            {
                return Err.Of(this).Message(ex.Message);
            }
            ret = StringHelper.EnsureNotNull(ret).Trim().ToUpperInvariant();
            if(!ret.Equals("VERIFIED"))
                return Err.Of(this).NotAllowed();

            //get status
            string statusStr = ipn["payment_status"];
            if(String.IsNullOrWhiteSpace(statusStr))
                return Err.Of(this).NotValid("payment_status");
            statusStr = statusStr.ToUpperInvariant();
            switch(statusStr)
            {
                case "DENIED":
                case "EXPIRED":
                case "FAILED":
                case "VOIDED":
                    status = PaymentFlags.Voided;
                    break;
                case "REFUNDED":
                    status = PaymentFlags.Refunded;
                    break;
                case "PENDING":
                    if(ipn.ContainsKey("pending_reason"))
                    {
                        string reason = ipn["pending_reason"];
                        reason = StringHelper.EnsureNotNull(reason);
                        reason = reason.ToUpperInvariant();
                        if(reason.Equals("AUTHORIZATION"))
                            status = PaymentFlags.Authorized;
                    }
                    break;
                case "PROCESSED":
                case "COMPLETED":
                    status = PaymentFlags.Captured;
                    break;
            }
            return String.Empty;

        }

        #endregion
    }
}
